from sklearn.naive_bayes import MultinomialNB, ComplementNB
from sklearn.linear_model import LogisticRegression, SGDClassifier, RidgeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import LinearSVC
from sklearn.preprocessing import LabelEncoder
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import (
train_test_split, cross_val_score, StratifiedKFold,
)
import numpy as np
import pandas as pd
np.random.seed(42)
from yellowbrick.model_selection import LearningCurve
import plotly.express as px
from tqdm import tqdm
import string
import time
import re
import nltk
from nltk.corpus import stopwords
from natasha import Segmenter, MorphVocab, NewsEmbedding, NewsMorphTagger, Doc
import optuna
import torch
from transformers import (
AutoModelForSequenceClassification, AutoTokenizer, TrainingArguments, Trainer
)
import opendatasets as od
from datasets import Dataset
import evaluate
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
Работу выполнил: Таратин Артём ПМ22-1
Датасет: Russian Social Media Text Classification
Введение
На данный момент машинное обучение играет ключевую роль в обработке больших данных в задачах обработки естественного языка, в том числе и распознавании тем текстов. Способность алгоритмов эффективно выполнять человеческую работу в этой сфере открывает новые горизонты для большого количества сфер деятельности.
Проблемой исследования является достижение хороших показателей метрик в данной задаче, что в силу многообразия языковых конструкций является довольно непростой задачей.
В работе раскрывается тема классификации текстов с предобработкой датасета и применением различных способов, моделей машинного обучения.
Целью работы будет разработка модели для классификации текстов по 13 категориям с максимально возможным значением метрики accuracy_score() на датасете Russian Social Media Text Classification с использованием различного инструментария.
Используются методы предобработки и векторизации текста, различные модели машинного обучения из библиотеки scikit-learn, модели, основанные на нейронных сетях, а также и другие инструменты машинного обучения.
Работа поможет понять теоретическую часть механизмов классификации текстов, а также продемонстрирует их применение. Разработанные методы смогут найти применение в любых других задачах, связанных с классификацией русских текстов из социальных сетей.
Глава 1. Основа
1.1 Введение в NLP и классификацию текстов¶
NLP (Natural Language Processing) - Обработка естественного языка это направление в машинном обучении, фокусирующееся на обработке, распознавании и генерации текста для различного рода задач. NLP тесно связана не только с машинным обучением, но и с лингвистикой.
На данный момент, NLP используется повсюду: Chat GPT, YandexGPT, поиск в Яндексе, Google, персональный помощник Алиса, Маруся и т.д. Такой подход позволяет значительно облегчить и упростить некоторые задачи, что также используется в коммерции.
Наиболее известными задачами NLP являются:
- Text Classification – классификация текстов по заранее определённым категориям
- Question Answering – ответы на вопросы по заданному контексту
- Translation – перевод предложений с различных языков
- Summarization – сжатие длинных документов в краткое изложение
- Text Generation – предоставляют возможность написания текста по любому запросу
1.2 Методы векторизации текста¶
Векторизация подразумевает под собой преобразование входных данных в векторы или тензоры действительных чисел, которые понятны моделям машинного обучения. Существует большое количество методов векторизации текста, все они отличаются между собой механизмом действия.
Мешок слов¶
Самый простой из всех существующих способов векторизации.
Алгоритм состоит из несколько шагов:
- Токенизация – из предложения извлекаются слова (токены)
- Создание словаря – создаётся одномерный массив из всех уникальных токенов
- Создание вектора – каждому токену из предложения присваивается 1, если токен присутствует в предложении, 0 иначе
TF-IDF¶
TF-IDF (Term Frequency-Inverse Document Frequency) – это числовой статистический показатель, который учитывает важность слова для документа. Алгоритм основан также на частотности как и мешок слов, но в нём используются более сложные методы вычисления.
Разберем TF-IDF по словам. TF означает Term Frequency (частотность термина), то есть нормализированный показатель частоты.
$ TF = \frac{Частота\ использования\ слова\ в\ документе}{Общее\ количество\ слов\ в\ документе} $
IDF означает Inversed Document Frequency (обратная частота документа).
$ DF = \frac{Документы\ содержащие\ слово}{Общее\ количество\ документов} $
$ IDF = \log(\frac{Документы\ содержащие\ слово}{Общее\ количество\ документов}) $
В итоге TF-IDF можно записать так:
$ TF-IDF = TF * IDF $
1.3 Описание используемых библиотек¶
sklearn,torch,transformers: Библиотеки машинного обученияopendatasets,datasets: Парсинг и работа с датасетамиevaluate: Метрики для оценки моделейnumpyиpandas: Анализ и обработка данныхyellowbrick: Визуализация результатов машинного обученияplotly: Визуализация данныхnltk: Обработка естественного языкаnatasha: Обработка текста на русском языкеoptuna: Оптимизация параметров моделейtqdm: Отображение индикатора выполнения
Глава 2. Работа с датасетом
2.1 Описание Датасета¶
Оригинал:¶
VKontakte communities can belong to one of several predefined categories. But even among the sports communities there is a fairly strong division by subject! The same authors can write about only one sport or at once about a large number. Based on a given set of posts, determine the topic – what kind of sport is being discussed in the selected community?
Перевод:¶
Сообщества ВКонтакте могут относиться к одной из нескольких предопределенных категорий. Но даже среди спортивных сообществ существует довольно строгое разделение по тематике! Одни и те же авторы могут писать только об одном виде спорта или сразу о большом количестве. Основываясь на заданном наборе постов, определите тему – какой вид спорта обсуждается в выбранном сообществе?
Список доступных категорий (13):¶
- athletics – легкая атлетика,
- autosport – автоспорт,
- basketball – баскетбол,
- boardgames – настольные игры,
- esport – киберспорт,
- extreme – экстрим,
- football – футбол,
- hockey – хоккей,
- martial_arts – боевые искусства,
- motosport – автоспорт,
- tennis – теннис,
- volleyball – волейбол,
- winter_sport – зимний спорт
Функция потерь выглядит так:¶
def score(true, pred, n_samples):
counter = 0
if true == pred:
counter += 1
else:
counter -= 1
return counter / n_samples
2.2 Анализ, Предобработка данных и вывод основных характеристик¶
Загружаем датасет с kaggle.com
od.download('https://www.kaggle.com/datasets/mikhailma/russian-social-media-text-classification')
Skipping, found downloaded files in "./russian-social-media-text-classification" (use force=True to force download)
Считываем данные и выводим первые 5 строк
train_data = pd.read_csv('./russian-social-media-text-classification/train.csv')
test_data = pd.read_csv('./russian-social-media-text-classification/test.csv')
train_data.head()
| oid | category | text | |
|---|---|---|---|
| 0 | 365271984 | winter_sport | Волшебные фото Виктория Поплавская ЕвгенияМедв... |
| 1 | 503385563 | extreme | Возвращение в подземелье Треша 33 Эйфория тупо... |
| 2 | 146016084 | football | Лучшие чешские вратари – Доминик Доминатор Гаш... |
| 3 | 933865449 | boardgames | Rtokenoid Warhammer40k валрак решил нас подкор... |
| 4 | 713550145 | hockey | Шестеркин затаскивает Рейнджерс в финал Восточ... |
train_data.shape, train_data.category.unique().size
((38740, 3), 13)
train_data.category.unique()
array(['winter_sport', 'extreme', 'football', 'boardgames', 'hockey',
'esport', 'athletics', 'motosport', 'basketball', 'tennis',
'autosport', 'martial_arts', 'volleyball'], dtype=object)
train_data.dtypes
oid int64 category object text object dtype: object
train_data.isna().sum()
oid 0 category 0 text 0 dtype: int64
Лишних столбцов нету, также как и выбросов
train_data.duplicated('text').sum()
2966
Но в то же время присутствуют дубликаты, которые лучше убрать, либо оставить первый
train_data.drop_duplicates('text', keep='first', inplace=True) # Оставляем первый экземпляр
train_data.duplicated('text').sum()
0
Используем LabelEncoder() для преобразования целевого столбца
le = LabelEncoder()
train_data['category_le'] = le.fit_transform(train_data.category)
train_data.head()
| oid | category | text | category_le | |
|---|---|---|---|---|
| 0 | 365271984 | winter_sport | Волшебные фото Виктория Поплавская ЕвгенияМедв... | 12 |
| 1 | 503385563 | extreme | Возвращение в подземелье Треша 33 Эйфория тупо... | 5 |
| 2 | 146016084 | football | Лучшие чешские вратари – Доминик Доминатор Гаш... | 6 |
| 3 | 933865449 | boardgames | Rtokenoid Warhammer40k валрак решил нас подкор... | 3 |
| 4 | 713550145 | hockey | Шестеркин затаскивает Рейнджерс в финал Восточ... | 7 |
Теперь выведем распределение классов и укажем количество их использований
sr = train_data.category.value_counts(sort=True, ascending=True)
xp, yp = np.array(sr), np.array(sr.index)
fig = px.bar(
x=xp, y=yp, orientation='h', text=yp, text_auto=True, color=xp,
title='Количество Текстов по Категориям', height=700,
color_continuous_scale=px.colors.sequential.dense,
)
fig.update_layout(
xaxis_tickvals=np.arange(0, 3001, 250), title_x=0.5,
xaxis_title='Количество', yaxis_title='Категория', showlegend=False,
)
fig.show()
Построим гистограмму распределения количества символов в предложениях
train_data['length'] = train_data['text'].str.len()
train_data.head(3)
| oid | category | text | category_le | length | |
|---|---|---|---|---|---|
| 0 | 365271984 | winter_sport | Волшебные фото Виктория Поплавская ЕвгенияМедв... | 12 | 65 |
| 1 | 503385563 | extreme | Возвращение в подземелье Треша 33 Эйфория тупо... | 5 | 246 |
| 2 | 146016084 | football | Лучшие чешские вратари – Доминик Доминатор Гаш... | 6 | 704 |
fig = px.histogram(
x=train_data['length'], title='Распределение Количества Символов', height=500,
color_discrete_sequence=[px.colors.sequential.Blues[-3]]
)
fig.update_layout(
xaxis_tickvals=np.arange(0, 3001, 250), xaxis_title='Количество',
yaxis_title='Категория', title_x=0.5,
)
fig.show()
Согласно графику, распределение категорий довольно близко к равномерному. В таком случае можно использовать метрику accuracy_score() т.к. даже при выборе самого популярного класса точность будет достаточно низкой. В любом случае, метрика score(), предложенная создателями датасета, по своему механизму работы очень похожа на accuracy_score(). Теперь посмотрим на самые популярные и менее популярные слова в текстах.
word_list = re.findall(r'\b\w{2,}\b', ' '.join(train_data.text.to_numpy()).lower())
df_words = pd.DataFrame(word_list, columns=['word']).groupby('word').size()
df_words = df_words.reset_index(name='count').sort_values('count', ascending=False)
pd.concat([df_words.head(25).reset_index(drop=True), df_words.tail(25).reset_index(drop=True)], axis=1)
| word | count | word | count | |
|---|---|---|---|---|
| 0 | на | 39638 | манипулировал | 1 |
| 1 | не | 23635 | мансабын | 1 |
| 2 | что | 19917 | мансап | 1 |
| 3 | 33 | 19756 | манфорд | 1 |
| 4 | по | 14054 | манси | 1 |
| 5 | за | 11337 | мануэля | 1 |
| 6 | это | 10827 | мануэлю | 1 |
| 7 | для | 9919 | мануэла | 1 |
| 8 | из | 9508 | мануальный | 1 |
| 9 | но | 9212 | мануалы | 1 |
| 10 | как | 8540 | мануала | 1 |
| 11 | мы | 7755 | мантуана | 1 |
| 12 | все | 7677 | мантуан | 1 |
| 13 | он | 7371 | мантикору | 1 |
| 14 | от | 6381 | мантикора | 1 |
| 15 | то | 6172 | мантика | 1 |
| 16 | до | 5168 | мансуровичем | 1 |
| 17 | будет | 4750 | мансурович | 1 |
| 18 | его | 4666 | мансуров | 1 |
| 19 | так | 4493 | мансури | 1 |
| 20 | после | 4167 | мансур | 1 |
| 21 | вы | 4156 | мансолдо | 1 |
| 22 | 2022 | 4150 | мансийским | 1 |
| 23 | tokentokenoid | 4144 | мансийский | 1 |
| 24 | если | 4101 | 龍王 | 1 |
В дополнение ко всему, нужно исключить слова, которые не несут никакой смысловой нагрузки. Для этого используем библиотеку nltk и nltk.corpus.stopwords.
nltk.download('stopwords')
russian_stopwords = stopwords.words('russian')
russian_stopwords[:10], len(russian_stopwords)
[nltk_data] Downloading package stopwords to /Users/admin/nltk_data... [nltk_data] Package stopwords is already up-to-date!
(['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со'], 151)
Со стоп словами
fig = px.treemap(df_words.head(275), path=['word'], values='count')
fig.show()
После удаления стоп слов
fig = px.treemap(df_words[~df_words.word.isin(russian_stopwords)].head(275), path=['word'], values='count')
fig.show()
Одними из самых популярных слов оказались предлоги, они не влияют на качество классификации, так как присутствуют в большинстве классов. Но даже после удаления стоп слов можно заметить числа и слова с основой token, всё это также не несёт смысловой нагрузки. Конечный вариант соберём на этапе предобработки датасета с кастомными фильтрами.
fig = px.histogram(
x=np.log2(df_words['count']), title='Распределение Слов по Частоте Употребления', height=500,
color_discrete_sequence=[px.colors.sequential.Blues[-3]], nbins=30
)
fig.update_layout(
xaxis_tickvals=np.arange(0, 16, 1), xaxis_title='Логарифм количества употреблений слов (X)',
yaxis_title='Количество слов употреблённых 2^X раз', title_x=0.5,
)
fig.show()
Большинство слов используются всего 1-4 раз.
Теперь посмотрим на распределение количества символов для каждого класса.
fig = px.box(data_frame=train_data, x='category', y='length', color='category')
fig.update_traces(marker_color=px.colors.sequential.Blues[-3])
fig.update_layout(
title='Количество Символов в Предложениях', showlegend=False,
xaxis_title='Класс', yaxis_title='Количество Символов', title_x=0.5,
)
fig.show()
2.3 Финальная Подготовка Датасета¶
Ещё раз инициализируем стоп слова
russian_stopwords = stopwords.words('russian')
russian_stopwords.extend(['это', 'tokentokenoid', 'rtokenoid', 'tokenoid', 'tokenoidtokenoid', 'votokenoid', 'evgentokenoid', '–'])
russian_stopwords[:10], len(russian_stopwords)
(['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со'], 159)
unallowed_chars = list(chr(i) for i in [*np.arange(ord('а'), ord('я')+1), *np.arange(ord('А'), ord('Я')+1), ord('ё'), ord('Ё')]) + list(string.ascii_letters)
russian_stopwords.extend(string.punctuation)
russian_stopwords.extend(unallowed_chars)
russian_stopwords[:10], len(russian_stopwords)
(['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со'], 309)
Пишем собственную функцию для токенизации предложений, удаления стоп слов, знаков и лемматизации слов
segmenter = Segmenter()
morph_vocab = MorphVocab()
emb = NewsEmbedding()
morph_tagger = NewsMorphTagger(emb)
def is_sublist(lst1, lst2):
return set(lst1) <= set(lst2)
def tokenize(text):
doc = Doc(text)
doc.segment(segmenter)
doc.tag_morph(morph_tagger)
for token in doc.tokens:
token.lemmatize(morph_vocab)
tokens = [
i.lemma.strip() for i in doc.tokens
if i.lemma.strip() not in russian_stopwords and
not i.lemma.strip().isnumeric()
]
return tokens
Добавляем новый столбец с обработанным текстом
%%time
train_data['text_tokenized'] = list(map(tokenize, train_data.text))
train_data['text_tokenized_joined'] = train_data['text_tokenized'].str.join(' ')
test_data['text_tokenized'] = list(map(tokenize, test_data.text))
test_data['text_tokenized_joined'] = test_data['text_tokenized'].str.join(' ')
CPU times: user 44min 23s, sys: 31.1 s, total: 44min 54s Wall time: 6min 14s
Поделим данные на тренировочную, валидационную и тестовую выборки.
X, y = train_data.text_tokenized.to_numpy(), train_data.category_le.to_numpy()
X_train, X_test_val, y_train, y_test_val = train_test_split(X, y, test_size=0.4, random_state=42, stratify=y)
X_test, X_val, y_test, y_val = train_test_split(X_test_val, y_test_val, test_size=0.5, random_state=42, stratify=y_test_val)
del X_test_val, y_test_val # удалим эти переменные, чтобы их после случайно не использовать
Глава 3. Обучение моделей для классификации текстов
Для векторизации текстов и оценки важности слова в контексте документа будем использовать статистическую меру TF-IDF. Для оптимизации работы используем функцию для обучения и оценки моделей с использованием Pipeline.
models = [
LogisticRegression(n_jobs=-1, random_state=42),
ComplementNB(),
SGDClassifier(n_jobs=-1, random_state=42),
RandomForestClassifier(n_jobs=-1, random_state=42),
RidgeClassifier(random_state=42),
KNeighborsClassifier(n_neighbors=10, n_jobs=-1),
LinearSVC(dual='auto'),
MultinomialNB(),
]
def fit_model(user_model):
model = Pipeline([
('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
('model', user_model),
]).fit(X_train, y_train)
y_pred = model.predict(X_val)
score = accuracy_score(y_pred, y_val)
return model, score
def cv_model(user_model):
model = Pipeline([
('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
('model', user_model),
]).fit(X_train, y_train)
kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
return model, score
Модели из списка ниже подбирались руками по двум критериям: точность предсказаний и скорость обучения. Также для сравнения были включены несколько стандартных решений для классификации.
3.1 LogisticRegression¶
%%time
model, score = cv_model(models[0])
print(f'{model[1].__class__.__name__}: {score:.4f}')
LogisticRegression: 0.8395 CPU times: user 1.62 s, sys: 246 ms, total: 1.86 s Wall time: 27.4 s
3.2 ComplementNB¶
%%time
model, score = cv_model(models[1])
print(f'{model[1].__class__.__name__}: {score:.4f}')
ComplementNB: 0.8572 CPU times: user 1.58 s, sys: 57.3 ms, total: 1.64 s Wall time: 1.67 s
3.3 SGDClassifier¶
%%time
model, score = cv_model(models[2])
print(f'{model[1].__class__.__name__}: {score:.4f}')
SGDClassifier: 0.8612 CPU times: user 2.68 s, sys: 45.5 ms, total: 2.72 s Wall time: 1.83 s
3.4 RandomForestClassifier¶
%%time
model, score = cv_model(models[3])
print(f'{model[1].__class__.__name__}: {score:.4f}')
RandomForestClassifier: 0.7937 CPU times: user 2min 50s, sys: 914 ms, total: 2min 51s Wall time: 26.3 s
3.5 RidgeClassifier¶
%%time
model, score = cv_model(models[4])
print(f'{model[1].__class__.__name__}: {score:.4f}')
RidgeClassifier: 0.8632 CPU times: user 9.29 s, sys: 70.8 ms, total: 9.36 s Wall time: 4.39 s
3.6 KNeighborsClassifier¶
%%time
model, score = cv_model(models[5])
print(f'{model[1].__class__.__name__}: {score:.4f}')
KNeighborsClassifier: 0.8001 CPU times: user 11.9 s, sys: 2.32 s, total: 14.2 s Wall time: 8.04 s
3.7 LinearSVC¶
%%time
model, score = cv_model(models[6])
print(f'{model[1].__class__.__name__}: {score:.4f}')
LinearSVC: 0.8590 CPU times: user 3.15 s, sys: 29.9 ms, total: 3.18 s Wall time: 3.2 s
3.8 MultinomialNB¶
%%time
model, score = cv_model(models[7])
print(f'{model[1].__class__.__name__}: {score:.4f}')
MultinomialNB: 0.8265 CPU times: user 1.66 s, sys: 23.9 ms, total: 1.69 s Wall time: 1.72 s
3.9 Обучение Всех Моделей¶
print(' ' + '_'*49)
all_models_scores = []
for model in models:
model_name = model.__class__.__name__
print(f'| Модель {model_name:<40} |')
start_time = time.time()
model, score = cv_model(model)
end_time = time.time()
learning_time = end_time - start_time
print(f'| {score=:.4f} {learning_time=:<18.4f} |', end=f"\n {'_'*49}\n")
all_models_scores.append({'name': model_name, 'score': score, 'time': learning_time})
_________________________________________________ | Модель LogisticRegression | | score=0.8395 learning_time=28.9804 | _________________________________________________ | Модель ComplementNB | | score=0.8572 learning_time=1.6207 | _________________________________________________ | Модель SGDClassifier | | score=0.8612 learning_time=1.7788 | _________________________________________________ | Модель RandomForestClassifier | | score=0.7937 learning_time=25.7424 | _________________________________________________ | Модель RidgeClassifier | | score=0.8632 learning_time=4.3032 | _________________________________________________ | Модель KNeighborsClassifier | | score=0.8001 learning_time=7.9358 | _________________________________________________ | Модель LinearSVC | | score=0.8590 learning_time=3.1977 | _________________________________________________ | Модель MultinomialNB | | score=0.8265 learning_time=1.5666 | _________________________________________________
df_models = pd.DataFrame(all_models_scores)
df_models = df_models.sort_values('score', ascending=False).reset_index(drop=True)
df_models
| name | score | time | |
|---|---|---|---|
| 0 | RidgeClassifier | 0.863213 | 4.303212 |
| 1 | SGDClassifier | 0.861210 | 1.778780 |
| 2 | LinearSVC | 0.858973 | 3.197689 |
| 3 | ComplementNB | 0.857203 | 1.620651 |
| 4 | LogisticRegression | 0.839499 | 28.980408 |
| 5 | MultinomialNB | 0.826500 | 1.566625 |
| 6 | KNeighborsClassifier | 0.800084 | 7.935763 |
| 7 | RandomForestClassifier | 0.793747 | 25.742447 |
Посмотрим на лучшую модель, это метрика для оценки моделей, предложенная создателями датасета
best_model, score = cv_model(models[4])
y_pred = best_model.predict(X_val)
best_model[1].__class__.__name__
'RidgeClassifier'
print(classification_report(y_val, y_pred))
precision recall f1-score support
0 0.89 0.89 0.89 518
1 0.89 0.87 0.88 603
2 0.93 0.88 0.90 490
3 0.91 0.97 0.94 505
4 0.82 0.81 0.81 543
5 0.72 0.80 0.76 552
6 0.84 0.85 0.84 557
7 0.91 0.87 0.89 580
8 0.81 0.80 0.81 577
9 0.90 0.92 0.91 544
10 0.97 0.95 0.96 586
11 0.92 0.86 0.89 554
12 0.87 0.89 0.88 546
accuracy 0.87 7155
macro avg 0.88 0.87 0.87 7155
weighted avg 0.87 0.87 0.87 7155
Модель ошибается примерно одинаково на каждом классе
cm = confusion_matrix(y_val, y_pred)
fig = px.imshow(cm, x=le.classes_, y=le.classes_, text_auto=True, width=750, height=750)
fig.update_layout(title=f'Confusion Matrix Heatmap – {best_model[1].__class__.__name__}', xaxis_title='Предсказание', yaxis_title='Правда', title_x=0.5)
fig.show()
%%time
visualizer = LearningCurve(best_model, scoring='accuracy').fit(X, y).show()
CPU times: user 40.4 s, sys: 211 ms, total: 40.6 s Wall time: 20.6 s
Но тут возникает проблема – переобучение
3.10 Результаты и выводы¶
df_models
| name | score | time | |
|---|---|---|---|
| 0 | RidgeClassifier | 0.863213 | 4.303212 |
| 1 | SGDClassifier | 0.861210 | 1.778780 |
| 2 | LinearSVC | 0.858973 | 3.197689 |
| 3 | ComplementNB | 0.857203 | 1.620651 |
| 4 | LogisticRegression | 0.839499 | 28.980408 |
| 5 | MultinomialNB | 0.826500 | 1.566625 |
| 6 | KNeighborsClassifier | 0.800084 | 7.935763 |
| 7 | RandomForestClassifier | 0.793747 | 25.742447 |
Судя по датафрейму, были получены хорошие результаты для классификации по 13 классам. К слову, некоторые стандартные модели отсутствуют в списке по причине очень долгого обучения и низких метрик, к примеру: MLPClassifier, GradientBoostingClassifier, SVC и т.п.
Глава 4. Улучшение и усовершенствование моделей
4.1 Выбор нескольких наиболее перспективных моделей на основе полученных результатов¶
Исходя из проведённых тестов, можно выделить несколько моделей, которые очнь хорошо себя показали
Ими являются:
RidgeClassifierSGDClassifierLinearSVC
Так как все модели обучаются довольно быстро, следующим шагом можно найти оптимальные гиперпараметры для каждой.
4.2 RidgeClassifier¶
Для начала оценим стандартную модель
model, score = fit_model(RidgeClassifier())
print(f'{model[1].__class__.__name__}: {score:.4f}')
RidgeClassifier: 0.8732
Теперь создадим функцию для поиска оптимальных гиперпараметров
def objective(trial):
params = {
'alpha': trial.suggest_float('alpha', 1e-2, 8, log=True),
'tol': trial.suggest_float('tol', 1e-5, 1e-1, log=True),
'solver': trial.suggest_categorical('solver', ['auto', 'cholesky', 'lsqr', 'sparse_cg', 'sag', 'saga']),
'max_iter': 5000,
}
if params['solver'] in ['svd', 'saga', 'cholesky']:
params['fit_intercept'] = False
model = Pipeline([
('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
('model', RidgeClassifier(**params)),
])
kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
return score
study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)
[I 2024-05-06 12:27:45,354] A new study created in memory with name: no-name-b7cae6f6-509a-4987-ac55-8bfbfd3af286
# study.best_value, study.best_params
Лучшая модель выглядит так:
(0.8640980711789646,
{'alpha': 1.1234072317796635,
'tol': 0.043383626047401036,
'solver': 'sparse_cg'})
best_params = {'alpha': 1.1234072317796635, 'tol': 0.043383626047401036, 'solver': 'sparse_cg'}
model, score_2 = fit_model(RidgeClassifier(**best_params))
print(f'{model[1].__class__.__name__}: {score_2:.4f}\nOld: {score:.4f}')
RidgeClassifier: 0.8718 Old: 0.8732
4.3 SGDClassifier¶
model, score = fit_model(SGDClassifier())
print(f'{model[1].__class__.__name__}: {score:.4f}')
SGDClassifier: 0.8671
def objective(trial):
params = {
'loss': trial.suggest_categorical('loss', ['log_loss', 'huber', 'modified_huber', 'epsilon_insensitive', 'hinge', 'perceptron']),
'alpha': trial.suggest_float('alpha', 1e-6, 8, log=True),
'penalty': trial.suggest_categorical('penalty', ['elasticnet', 'l1', 'l2']),
'l1_ratio': trial.suggest_float('l1_ratio', 0, 1),
'max_iter': 10000
}
model = Pipeline([
('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
('model', SGDClassifier(**params)),
])
kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
return score
study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)
[I 2024-05-06 12:27:46,649] A new study created in memory with name: no-name-1d00f4ac-459c-4916-a0c2-e48dbe190145
# study.best_value, study.best_params
Лучшая модель выглядит так:
(0.8624209114775152,
{'loss': 'hinge',
'alpha': 7.966067426682004e-05,
'penalty': 'l2',
'l1_ratio': 0.9717104158510521})
best_params = {'loss': 'hinge', 'alpha': 7.966067426682004e-05, 'penalty': 'l2', 'l1_ratio': 0.9717104158510521}
model, score_2 = fit_model(SGDClassifier(**best_params))
print(f'{model[1].__class__.__name__}: {score_2:.4f}\nOld: {score:.4f}')
SGDClassifier: 0.8671 Old: 0.8671
4.4 LinearSVC¶
model, score = fit_model(LinearSVC(max_iter=10000, dual='auto'))
print(f'{model[1].__class__.__name__}: {score:.4f}')
LinearSVC: 0.8678
def objective(trial):
params = {
'C': trial.suggest_float('C', 1e-5, 8, log=True),
'tol': trial.suggest_float('tol', 1e-5, 1e-1, log=True),
'dual': trial.suggest_categorical('dual', [True, False]),
'max_iter': 20000,
}
model = Pipeline([
('tfidf', TfidfVectorizer(tokenizer=lambda x: x, preprocessor=lambda x: x, token_pattern=None)),
('model', LinearSVC(**params)),
])
kfold = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)
score = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy').mean()
return score
study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=100)
[I 2024-05-06 12:27:47,983] A new study created in memory with name: no-name-b04b7520-5a4d-46fb-a4ee-f148895f5487
# study.best_value, study.best_params
Лучшая модель выглядит так:
(0.8620946906568248,
{'C': 0.278178921051732,
'tol': 0.004207430596057012,
'dual': True})
best_params = {'C': 0.278178921051732, 'tol': 0.004207430596057012, 'dual': True}
model, score_2 = fit_model(LinearSVC(**best_params))
print(f'{model[1].__class__.__name__}: {score_2:.4f}\nOld: {score:.4f}')
LinearSVC: 0.8688 Old: 0.8678
4.5 Результаты и выводы¶
Как видно из результатов поиск параметров почти не увеличил точность лучших моделей.
Возможно стоит рассмотреть другие подходы с применением нейронных сетей и различных способов векторизации текста.
Глава 5. Модели huggingface.co
Ещё одним подходом к классификации текстов является дообучение нейросетевых моделей. Так как язык датасета русский, нужно использовать модели с поддержкой русского языка.
В данной задаче лучшую результативность показали две модели:
- ruBert-large
- ruRoberta-large
Именно они и будут рассмотрены.
5.1 Подготовка к дообучению¶
Укажем девайс на котором будем производить все операции, в данном случае это GPU (cuda)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device
device(type='cuda')
train_data.head(3)
| oid | category | text | category_le | length | text_tokenized | text_tokenized_joined | |
|---|---|---|---|---|---|---|---|
| 0 | 365271984 | winter_sport | Волшебные фото Виктория Поплавская ЕвгенияМедв... | 12 | 65 | [волшебный, фото, виктория, поплавский, евгени... | волшебный фото виктория поплавский евгениямедв... |
| 1 | 503385563 | extreme | Возвращение в подземелье Треша 33 Эйфория тупо... | 5 | 246 | [возвращение, подземелье, треш, эйфория, тупос... | возвращение подземелье треш эйфория тупость жа... |
| 2 | 146016084 | football | Лучшие чешские вратари – Доминик Доминатор Гаш... | 6 | 704 | [хороший, чешский, вратарь, доминик, доминатор... | хороший чешский вратарь доминик доминатор гаше... |
Разделим датасет на 3 выборки в соотношении 80/10/10% и преобразуем их в объекты класса Dataset
X, y = train_data['text'].tolist(), train_data['category_le'].tolist()
X_train, X_test_val, y_train, y_test_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
X_test, X_val, y_test, y_val = train_test_split(X_test_val, y_test_val, test_size=0.5, random_state=42, stratify=y_test_val)
del X_test_val, y_test_val # удалим эти переменные, чтобы их после случайно не использовать
data_dict_train = {'text': X_train, 'label': y_train}
data_dict_val = {'text': X_val, 'label': y_val}
data_dict_test = {'text': X_test, 'label': y_test}
dataset_train = Dataset.from_dict(data_dict_train)
dataset_val = Dataset.from_dict(data_dict_val)
dataset_test = Dataset.from_dict(data_dict_test)
dataset_train
Dataset({
features: ['text', 'label'],
num_rows: 28619
})
В предыдущей части работы проводилась предобработка датасета, были удалены стоп слова, ненужные числа, была использована лемматизация, но в данном случае всё это не требуется, так как токенизатор модели выполняет эту работу
data = dataset_train[:5]
for text, label in zip(data['text'], data['label']):
le_label = le.inverse_transform([label])[0]
print(f'{le_label:^14} | {text}')
volleyball | Мэтью Андерсон возвращается в Казань 33 Но в составе другой команды. ‼Не пропустите сегодняшний матч. который обещает быть максимально жарким 33 Зенит Казань Казань Зенит Санкт Петербург 19 30 29. 10. 2022 Прямая трансляция
hockey | Трактор Ltokenoid 33 I vs Авангард ️ ⠀ Гостем онлайн эфира станет экс нападающий Трактора и Авангарда Николай Лемтюгов. Михаил Зислис и Елизавета Смолина обсудят с хоккейным экспертом Денисом Мошаровым прошедший матч команды и поделятся впечатлениями об игре сегодняшнего соперника. ⠀ В конкурсе прогнозов вас ждут мнения журналистов изданий Спорт Экспресс и tokentokenoid Михаила Скрыля и Дарьи Тубольцевой. ⠀ Прямая трансляция студии в официальной группе клуба ВКонтакте и на Yotokenoid канале Трактор SHOW начнется за 35 минут перед началом матча и продолжится в каждом из перерывов. хктрактор толькочернобелый
esport | Три матча и три путевки на IEM Rtokenoid Major пятый и финальный раунд Etokenoid RMR A стартует совсем скоро 33 Страница турнира tokentokenoid IEM
extreme | Друзья кидайте в предложку свои фото и видео с тренировок и соревнований. Посмотрим кто на что горазд в нашей дружине Приветствую друзья 33 Значительно улучшил свой результат в отжиманиях на брусьях с дополнительным весом 32 кг на разы 33 Новому личному рекорду быть
winter_sport | ОМОН и Новый год К обеспечению безопасности гостей горнолыжного курорта Шерегеш в период новогодних праздников привлекли ОМОН. Для охраны общественного порядка и обеспечения безопасности гостей горнолыжного курорта задействуют силы и средства ОМОН. В новогоднюю ночь в регионе к охране общественного порядка были привлечены более 400 бойцов Росгвардии. Среди них подразделения ОМОН вневедомственной охраны и лицензионно разрешительной работы. Кроме Росгвардии правоохранительным органам оказали содействие работники частных охранных организаций а также представители добровольных народных дружин.
Также стоит подготовить метрику для оценки наших моделей, в данном случае это accuracy.
metric = evaluate.load('accuracy')
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
5.2 ruBert-large¶
Для начала тоекнизируем наш датасет
tokenizer = AutoTokenizer.from_pretrained('ai-forever/ruBert-large')
def tokenize_function(examples):
return tokenizer(examples['text'], padding='max_length', truncation=True, max_length=512)
dataset_train = dataset_train.map(tokenize_function, batched=True)
dataset_val = dataset_val.map(tokenize_function, batched=True)
dataset_test = dataset_test.map(tokenize_function, batched=True)
Map: 100%|██████████| 28619/28619 [00:07<00:00, 3719.53 examples/s] Map: 100%|██████████| 3578/3578 [00:00<00:00, 3945.21 examples/s] Map: 100%|██████████| 3577/3577 [00:00<00:00, 3913.20 examples/s]
После токенизации помимо столбцов 'text' и 'label' появились еще 'input_ids', 'token_type_ids', 'attention_mask'.
dataset_train
Dataset({
features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
num_rows: 28619
})
Датасет уже предобработан, осталось только загрузить предобученную модель.
Но перед дообучением нужно указать что:
num_labels=13- Количество классов нашей модели изменилосьignore_mismatched_sizes=True- Отключаем исключение при несовпадении некоторых весов
model = AutoModelForSequenceClassification.from_pretrained(
'ai-forever/ruBert-large', num_labels=13
).to(device)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at ai-forever/ruBert-large and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Зададим аргументы для дообучения модели при помощи объекта класса TrainingArguments
training_args = TrainingArguments(
num_train_epochs=4,
warmup_steps=500,
output_dir='hf_models',
evaluation_strategy='steps',
eval_steps=250,
save_strategy='steps',
save_steps=250,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
)
Создадим объект класса Trainer указав нашу модель, аргументы, датасеты и метрику
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
compute_metrics=compute_metrics,
)
c:\Основная папка\Рабочий стол\ml_coursework_2nd_year\.venv\Lib\site-packages\accelerate\accelerator.py:436: FutureWarning: Passing the following arguments to `Accelerator` is deprecated and will be removed in version 1.0 of Accelerate: dict_keys(['dispatch_batches', 'split_batches', 'even_batches', 'use_seedable_sampler']). Please pass an `accelerate.DataLoaderConfiguration` instead: dataloader_config = DataLoaderConfiguration(dispatch_batches=None, split_batches=False, even_batches=True, use_seedable_sampler=True)
А теперь запустим процесс дообучения
# trainer.train()
Лучшая модель выглядит так:
{
"epoch": 3.98,
"eval_accuracy": 0.8806595863610955,
"eval_loss": 0.7195928692817688,
"eval_runtime": 240.8435,
"eval_samples_per_second": 14.856,
"eval_steps_per_second": 1.86,
"step": 14250
}
Теперь загрузим чекпоинт лучшей модели, чтобы оценить качество предсказаний
model = AutoModelForSequenceClassification.from_pretrained(
'hf_models/ruBert-large-checkpoint-14250',
).to(device)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
compute_metrics=compute_metrics,
)
Оценим модель на всех 3-х выборках
- Тренировочная:
ix = np.random.choice(range(len(dataset_train)), size=5000, replace=False)
trainer.evaluate(dataset_train.select(ix))
100%|██████████| 625/625 [05:51<00:00, 1.78it/s]
{'eval_loss': 0.04601737856864929,
'eval_accuracy': 0.9912,
'eval_runtime': 352.1637,
'eval_samples_per_second': 14.198,
'eval_steps_per_second': 1.775}
- Валидационная:
trainer.evaluate(dataset_val)
100%|██████████| 448/448 [04:11<00:00, 1.78it/s]
{'eval_loss': 0.7195214033126831,
'eval_accuracy': 0.8806595863610955,
'eval_runtime': 252.0094,
'eval_samples_per_second': 14.198,
'eval_steps_per_second': 1.778}
- Тестовая:
trainer.evaluate(dataset_test)
100%|██████████| 448/448 [04:12<00:00, 1.77it/s]
{'eval_loss': 0.7208680510520935,
'eval_accuracy': 0.8803466592116299,
'eval_runtime': 253.3381,
'eval_samples_per_second': 14.119,
'eval_steps_per_second': 1.768}
Accuracy score на валидационной и тестовой выборках у данной модели немного выше (+0.7%) чем применение TF-IDF и моделей машинного обучения из библиотеки sklearn, но затраты времени и ресурсов на тренировку и предсказание значительно увеличилось.
Предскажем ещё раз тестовую выборку и выведем отчёт о классификации
predictions_output = trainer.predict(dataset_test)
logits = predictions_output.predictions
predicted_labels = np.argmax(logits, axis=-1)
100%|██████████| 448/448 [04:13<00:00, 1.77it/s]
print(classification_report(
le.inverse_transform(dataset_test['label']),
le.inverse_transform(predicted_labels),
))
precision recall f1-score support
athletics 0.91 0.90 0.90 259
autosport 0.88 0.88 0.88 301
basketball 0.88 0.84 0.86 245
boardgames 0.96 0.96 0.96 253
esport 0.85 0.84 0.85 272
extreme 0.77 0.82 0.79 276
football 0.83 0.84 0.84 278
hockey 0.86 0.89 0.88 289
martial_arts 0.84 0.84 0.84 288
motosport 0.96 0.94 0.95 272
tennis 0.95 0.94 0.95 293
volleyball 0.87 0.86 0.86 278
winter_sport 0.89 0.90 0.90 273
accuracy 0.88 3577
macro avg 0.88 0.88 0.88 3577
weighted avg 0.88 0.88 0.88 3577
Модель предсказывает все классы с примерно одинаковой точностью, что является хорошим показателем
5.3 ruRoberta-large¶
Совершим те же шаги, что и для предыдущей модели
tokenizer = AutoTokenizer.from_pretrained('ai-forever/ruRoberta-large')
def tokenize_function(examples):
return tokenizer(examples['text'], padding='max_length', truncation=True, max_length=512)
dataset_train = dataset_train.map(tokenize_function, batched=True)
dataset_val = dataset_val.map(tokenize_function, batched=True)
dataset_test = dataset_test.map(tokenize_function, batched=True)
Map: 100%|██████████| 28619/28619 [00:05<00:00, 5048.98 examples/s] Map: 100%|██████████| 3578/3578 [00:00<00:00, 5364.25 examples/s] Map: 100%|██████████| 3577/3577 [00:00<00:00, 5268.05 examples/s]
dataset_train
Dataset({
features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
num_rows: 28619
})
model = AutoModelForSequenceClassification.from_pretrained(
'ai-forever/ruRoberta-large', num_labels=13, ignore_mismatched_sizes=True,
).to(device)
Some weights of RobertaForSequenceClassification were not initialized from the model checkpoint at ai-forever/ruRoberta-large and are newly initialized: ['classifier.dense.bias', 'classifier.dense.weight', 'classifier.out_proj.bias', 'classifier.out_proj.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Аргументы тренировки подбираются для каждой модели отдельно
training_args = TrainingArguments(
num_train_epochs=10,
warmup_steps=500,
output_dir='hf_models',
evaluation_strategy='steps',
eval_steps=100,
save_strategy='steps',
save_steps=100,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
gradient_accumulation_steps=8,
fp16=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
compute_metrics=compute_metrics,
)
c:\Основная папка\Рабочий стол\ml_coursework_2nd_year\.venv\Lib\site-packages\accelerate\accelerator.py:436: FutureWarning: Passing the following arguments to `Accelerator` is deprecated and will be removed in version 1.0 of Accelerate: dict_keys(['dispatch_batches', 'split_batches', 'even_batches', 'use_seedable_sampler']). Please pass an `accelerate.DataLoaderConfiguration` instead: dataloader_config = DataLoaderConfiguration(dispatch_batches=None, split_batches=False, even_batches=True, use_seedable_sampler=True)
# trainer.train()
Лучшая модель выглядит так:
{
"epoch": 9.17,
"eval_accuracy": 0.8918390162101733,
"eval_loss": 0.6570751070976257,
"eval_runtime": 227.775,
"eval_samples_per_second": 15.708,
"eval_steps_per_second": 1.967,
"step": 4100
}
Также загрузим лучшую модель с чекпоинта
model = AutoModelForSequenceClassification.from_pretrained(
'hf_models/ruRoberta-large-checkpoint-4100',
).to(device)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
compute_metrics=compute_metrics,
)
И оценим модель на всех 3-х выборках
- Тренировочная:
ix = np.random.choice(range(len(dataset_train)), size=5000, replace=False)
trainer.evaluate(dataset_train.select(ix))
100%|██████████| 625/625 [05:21<00:00, 1.94it/s]
{'eval_loss': 0.004432776477187872,
'eval_accuracy': 0.9982,
'eval_runtime': 322.1358,
'eval_samples_per_second': 15.521,
'eval_steps_per_second': 1.94}
- Валидационная:
trainer.evaluate(dataset_val)
100%|██████████| 448/448 [03:47<00:00, 1.97it/s]
{'eval_loss': 0.6570751070976257,
'eval_accuracy': 0.8918390162101733,
'eval_runtime': 227.8208,
'eval_samples_per_second': 15.705,
'eval_steps_per_second': 1.966}
- Тестовая:
trainer.evaluate(dataset_test)
100%|██████████| 448/448 [03:48<00:00, 1.96it/s]
{'eval_loss': 0.6594090461730957,
'eval_accuracy': 0.8951635448700028,
'eval_runtime': 229.1215,
'eval_samples_per_second': 15.612,
'eval_steps_per_second': 1.955}
По сравнению с моделями sklearn точность предсказаний выросла на +2.2%, а это уже более весомый результат
Предскажем тестовую выборку и выведем отчёт о классификации
predictions_output = trainer.predict(dataset_test)
logits = predictions_output.predictions
predicted_labels = np.argmax(logits, axis=-1)
100%|██████████| 448/448 [03:50<00:00, 1.94it/s]
print(classification_report(
le.inverse_transform(dataset_test['label']),
le.inverse_transform(predicted_labels),
))
precision recall f1-score support
athletics 0.93 0.92 0.92 259
autosport 0.92 0.90 0.91 301
basketball 0.87 0.87 0.87 245
boardgames 0.97 0.95 0.96 253
esport 0.86 0.88 0.87 272
extreme 0.84 0.84 0.84 276
football 0.83 0.85 0.84 278
hockey 0.85 0.88 0.87 289
martial_arts 0.87 0.86 0.86 288
motosport 0.95 0.95 0.95 272
tennis 0.95 0.96 0.95 293
volleyball 0.89 0.88 0.89 278
winter_sport 0.92 0.89 0.91 273
accuracy 0.90 3577
macro avg 0.90 0.90 0.90 3577
weighted avg 0.90 0.90 0.90 3577
Ситуация по предсказанию всех классов аналогичная, только метрики чуть выше
Глава 6. Финальные результаты
По итогам можно составить такую таблицу
| Модель | Точность |
|---|---|
| ruRoberta-large | 0.8952 |
| ruBert-large | 0.8801 |
Разница между лучшими моделями sklearn и transformers составляет целых 2.2%, что может оказать большое влияение на выбор модели.
Заключение
В ходе работы получилось построить модели для классификации текстов используя различные подходы.
Модели классификации sklearn показали довольно высокие результаты в случае использования TF-IDF с дополнительной предобработкой данных.
Лучшими из них оказались:
| Модель | Точность |
|---|---|
| RidgeClassifier | 0.8732 |
| LinearSVC | 0.8678 |
| SGDClassifier | 0.8660 |
Также были применены нейросетевые модели классификации текстов такие как:
| Модель | Точность |
|---|---|
| ruRoberta-large | 0.8952 |
| ruBert-large | 0.8801 |
Любой из этих подходов гарантирует довольно высокую точность классификации текстов по 13 категориям. Модели sklearn показали высокое быстродействие, при обучении и предсказании используется только центральный процессор. Модели transformers используют центральный процессор в связке с видеокартой, они значительно тяжелее чем модели sklearn, но дают более точные результаты.
Список использованных источников
NLP (Natural Language Processing, обработка естественного языка)
Краткий обзор техник векторизации в NLP
Репозиторий в поддержку курса "Машинное обучение", читаемого в Финансовом университете
Инструменты для решения NER-задач для русского языка
Natural Language Processing With Python's NLTK Package
Библиотека Optuna в Python для оптимизации гиперпараметров
Шпаргалка по визуализации данных в Python с помощью Plotly
Руформеры (Список популярных открытых базовых моделей на основе трансформеров)